package simpletestbed.core;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;

import simpletestbed.TestCase;
import simpletestbed.TestResult;
import simpletestbed.utils.DateUtils;

public class TestServerCaller {
	
	public final static String CALLER_FUNC_NAME = "remoteFunc";
	
	
	public TestServerCaller() {
		
	}
	
	public byte[] remoteFunc(byte[] arg, boolean checkMemUsage, long timeout) {
		try {
			System.gc();
			
			ByteArrayInputStream bais = new ByteArrayInputStream(arg);
			DataInputStream dis = new DataInputStream(bais);
			// Read test cases
			int numTestCase = dis.readInt();
			TestCase[] testCases = new TestCase[numTestCase];
			TestCaseThread[][] threads = new TestCaseThread[numTestCase][];
			long[][] elapseTimes = new long[numTestCase][];
			for (int i=0; i<numTestCase; i++) {
				ObjectInputStream objStream = new ObjectInputStream(dis);
				TestCase testCase = (TestCase)objStream.readObject();
				testCases[i] = testCase;
				threads[i] = new TestCaseThread[testCase.getTimes()];
				elapseTimes[i] = new long[testCase.getTimes()];
				Arrays.fill(elapseTimes[i], 0);
			}
			dis.close();
			bais.close();
			
			// Start test cases
			for (int i=0; i<numTestCase; i++) {
				TestCase testCase =  testCases[i];
				System.out.println(DateUtils.now()+" Starting test case : "+testCase.getName());
				for (int j=0; j<testCase.getTimes(); j++) {
					threads[i][j] = new TestCaseThread((TestCase)testCase.clone());
					threads[i][j].start();
				}
			}
			// Self blocked
			Thread.sleep(50);
			
			int numFinishedTestCases = 0;
			int[] numFinishedThreads = new int[numTestCase];
			Arrays.fill(numFinishedThreads, 0);
			
			TestResult[] avgResults = new TestResult[numTestCase];
			TestResult[][] results = new TestResult[numTestCase][];
			for (int caseIdx=0; caseIdx<numTestCase; caseIdx++) {
				results[caseIdx] = new TestResult[testCases[caseIdx].getTimes()];
			}
			
			double[] avgRuntimes= new double[numTestCase];
			Arrays.fill(avgRuntimes, 0.0);
			
			double maxMemUsage = getMemoryUsage();
			
			// Loop and wait for all test cases finish
			while(numFinishedTestCases < numTestCase) {
				for (int caseIdx=0; caseIdx<numTestCase; caseIdx++) {
					TestCase testCase =  testCases[caseIdx];
					if (checkMemUsage) { 
						long memUsage  = getMemoryUsage();
						if (memUsage > maxMemUsage) {
							maxMemUsage = memUsage;
						}
					}
					
					if (numFinishedThreads[caseIdx] == testCase.getTimes()) { // Already finished?
						//System.out.println("case "+caseIdx+" is finished");
						continue; 
					}
					
					for (int threadIdx=0; threadIdx<testCase.getTimes(); threadIdx++) {
						TestCaseThread thread = threads[caseIdx][threadIdx];
						if (thread == null) { // If it is already destroyed
							//System.out.println("thread "+threadIdx+" is null");
							continue;
						}
						else if (thread.getTestingStatus() == TestCaseThread.STATUS_READY) {
							//System.out.println("thread "+threadIdx+" tid "+thread.getId()+" is not started");
							continue;
						}
						else if (thread.getTestingStatus() == TestCaseThread.STATUS_STARTED) { // If it is still running
							// Check if it is out of time
							if (timeout > 0) {
								long elapseTime = thread.getElapseTime();
								if (elapseTime > elapseTimes[caseIdx][threadIdx] + timeout) {
									// Notify elapse time
									System.out.println(DateUtils.now()+" Time Elapse: "+testCase.getName()+"[thread"+
											threadIdx+"] :"+ elapseTime/1000+" seconds");								
									// Interrupt this thread								
									elapseTimes[caseIdx][threadIdx] = elapseTime;
									// threads[caseIdx][threadIdx].interrupt();
									thread.stop();
									System.out.println(DateUtils.now()+" Thread["+threadIdx+"] has been stopped!");
								}
							}
							continue;
						}
						else { // Just finished
							results[caseIdx][threadIdx] = thread.getResult();
							avgRuntimes[caseIdx] += thread.getRuntime();
							numFinishedThreads[caseIdx]++;
							threads[caseIdx][threadIdx] = null;
							if (numFinishedThreads[caseIdx] == testCase.getTimes()) { 
								// Finished all threads of this test cases
								avgResults[caseIdx] = testCase.reduceResult(results[caseIdx]);
								avgRuntimes[caseIdx] /= testCase.getTimes();
								System.out.println(DateUtils.now()+" Test Result: "+testCase.getName()+" : "+avgResults[caseIdx]+ 
										" in "+avgRuntimes[caseIdx]+ " ms");
								numFinishedTestCases++;
							}
						}
					}
				}
				// Self blocked
				Thread.sleep(10);
			}
			
			// Write back the results
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(baos);
			for (int i=0; i<numTestCase; i++) {
				oos.writeObject(avgResults[i]);
				oos.writeDouble(avgRuntimes[i]);
			}
			if (checkMemUsage) {
				System.out.println(DateUtils.now()+" Peak Memory Size = "+maxMemUsage);
			}
			oos.writeDouble(maxMemUsage);
			oos.flush();
			byte[] result = baos.toByteArray();
			baos.close();

			System.gc();
			return result;
		}catch(Exception e) {
			e.printStackTrace();
			return null;
		}		
	}
	
	public static long getMemoryUsage() throws InterruptedException {
		System.gc();
		Thread.sleep(500);
		System.gc();
		long totalMemSize = Runtime.getRuntime().totalMemory();
		long freeMemSize = Runtime.getRuntime().freeMemory();
		return totalMemSize - freeMemSize;
	}
	
	static class TestCaseThread extends Thread {
		
		private TestCase testcase;
		
		private TestResult result = null;
		
		private long runtime = -1;
		
		private long startTime = -1;
		
		public static final int STATUS_READY = 0;
		
		public static final int STATUS_STARTED = 1;
		
		public static final int STATUS_FINISHED = 2;
		
		private int status = STATUS_READY;
		
		public TestCaseThread(TestCase testcase) {
			this.testcase = testcase;
		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			this.status = STATUS_STARTED;
			this.startTime = System.currentTimeMillis();
			this.result = this.testcase.run();
			long endTime = System.currentTimeMillis();
			this.runtime = endTime - startTime;
			this.status = STATUS_FINISHED;
		}
		
		public TestResult getResult() {
			return result;
		}
		
		public double getRuntime() {
			return runtime;
		}
		
		public long getElapseTime() {
			if (this.startTime == -1) {
				return 0;
			}
			
			long endTime = System.currentTimeMillis();
			return endTime - this.startTime;
		}
		
		public int getTestingStatus() {
			return this.status;
		}
	}

}
